將一些常用的widget組合提取出來,可以增加組件的複用性,在更動時也比較好維護。
這同時也考驗這個自定義的組件是否充分設計:有新的需求的時候,應該要可以直接擴充組件,而非對組件進行修改。
今天我們會以APOD專案中最常用到的「天文圖文描述組件」作為範例,將之提取出來建立成自定義的組件,在多個頁面重複使用。
好的,那我們就開始吧!
AstroPicture
。AstroPicture
可能會有三種使用情況:
AstroPicture
時,要提供以下對應資訊:
在lib
下創建widget
資料夾,創建新檔案AstroPicture
在 AstroPicture
設定好以下prop
class AstroPicture extends StatefulWidget {
final String title; // 標題
final String pictureUrl; // 圖片來源
final String desc; // 圖片描述
final String note; // 手札
final bool isFavorite; // 是否收藏
const AstroPicture(
{super.key,
required this.title,
required this.pictureUrl,
required this.desc,
this.note = '', // 非必要,若沒有手札紀錄預設為空字串
this.isFavorite = false // 非必要,預設為非收藏的圖片});
@override
State<AstroPicture> createState() => _AstroPictureState();
}
MainPage
拉過來,並將標題、圖片等會由外部傳入的資料改用 widget.PROP_NAME
顯示
// 定義頁面筆記模式
enum NoteType {
text,
editable,
}
class AstroPicture extends StatefulWidget {
final String title;
final String pictureUrl;
final String desc;
final String note;
final bool isFavorite;
const AstroPicture(
{super.key,
required this.title,
required this.pictureUrl,
required this.desc,
this.note = '',
this.isFavorite = false});
@override
State<AstroPicture> createState() => _AstroPictureState();
}
class _AstroPictureState extends State<AstroPicture> {
final TextEditingController _controller = TextEditingController();
NoteType _noteType = NoteType.editable;
@override
void initState() {
super.initState();
_controller.text = widget.note;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Size deviceScreen = MediaQuery.of(context).size;
return SingleChildScrollView(
child: Column(children: <Widget>[
Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
widget.title,
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.w500),
),
),
Stack(
children: [
SizedBox(
width: deviceScreen.width,
child: widget.pictureUrl.isNotEmpty
? Image.network(widget.pictureUrl, frameBuilder:
(context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded) {
return child;
}
return AnimatedOpacity(
opacity: frame == null ? 0 : 1,
duration: const Duration(seconds: 1),
curve: Curves.easeOut,
child: child,
);
})
: SizedBox(
width: deviceScreen.width,
height: deviceScreen.width,
child: const Center(
child: Text(
'圖片載入錯誤',
style: TextStyle(color: Colors.red, fontSize: 30),
)),
),
),
widget.pictureUrl.isNotEmpty
? Positioned(
top: 10.0,
right: 10.0,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white24),
onPressed: () {
print('add to favorite');
},
child: widget.isFavorite
? Icon(
Icons.favorite,
color: Colors.pink[200],
)
: const Icon(
Icons.favorite_border_outlined,
color: Colors.white,
)),
)
: Container(),
],
),
Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
widget.desc,
style: const TextStyle(fontSize: 16, color: Colors.blueGrey),
),
),
_noteType == NoteType.text
? Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(10.0),
child: Text(_controller.text))),
Container(
width: 100,
padding: const EdgeInsets.all(10),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
fixedSize: const Size(50, 50),
),
onPressed: () {
setState(() {
_noteType = NoteType.editable;
});
},
child: const Icon(Icons.edit)))
],
)
: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: TextField(
controller: _controller,
decoration: const InputDecoration(
filled: true,
fillColor: Colors.white,
contentPadding: EdgeInsets.all(10),
border: InputBorder.none,
),
maxLines: 5,
minLines: 3,
),
),
),
),
Container(
width: 100,
padding: const EdgeInsets.all(10),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
fixedSize: const Size(50, 50),
),
onPressed: () {
setState(() {
_noteType = NoteType.text;
});
},
child: const Icon(
Icons.save,
),
),
)
],
),
]),
);
}
}
MainPage
加入AstroPicture
widgetMainPage
原本放圖片的位置置入AstroPicture, 並給予對應的參數:
if (snapshot.hasData) {
ApodData? data = snapshot.data;
return AstroPicture(
title: data?.title ?? '',
pictureUrl: data?.url ?? '',
desc: data?.desc ?? '',
note: 'Place your note here!', // 待日後將儲存的筆記放進來
isFavorite: false, // 待日後將收藏狀態放進來
);
}
可以看到AstroPicture被提取出來,之後任何頁面都可以一起使用這個Widget,只要給予不一樣的參數就可以顯示不同的內容。FavoritePage
加入AstroPicture
ListView
設定為點擊項目之後,會導頁到AstroPicture
class FavoritePage extends StatefulWidget {
const FavoritePage({super.key});
@override
State<FavoritePage> createState() => _FavoritePageState();
}
class _FavoritePageState extends State<FavoritePage> {
@override
Widget build(BuildContext context) {
return Center(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100,
padding: const EdgeInsets.all(5.0),
child: SizedBox(
height: 100,
child: InkWell(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return Scaffold(
appBar: AppBar(
title: const Text('one of the title'),
),
body: const AstroPicture(
title: 'title of astro picture',
pictureUrl: '',
desc: 'desc',
note: "don't know what to fill yet",
isFavorite: false,
)); // 要填什麼資料進去呢??
}));
},
child: const Card(
elevation: 5.0,
child: Center(
child: Text(
'I am Image Title',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.w500),
),
)),
),
),
);
},
itemCount: 2,
),
);
}
}
Day16
相關commit今天先嘗試抽出AstroPicture
組件,將其放在MainPage
, 以及作為FavoritePage
點擊列表選項後的導頁目標。
可是,我們要怎麼在FavoritePage
得知我在MainPage
收藏了哪些頁面?導頁之後頁面上的資料又哪裡來呢?
明天一起來看看App狀態管理,將MainPage
收藏的天文資料存到APP層級,讓其他頁面也可以取得,就可以解決頁面間的資料溝通問題囉!